/*
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package tal.drivechain.system;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import tal.drivechain.wildcard.Wildcard;
import tal.drivechain.plugin.Logger;
import tal.drivechain.plugin.Module;
import tal.drivechain.plugin.ModulePackageManager;
import tal.drivechain.module.xmlconfig.LoggerInfo;
import tal.drivechain.module.xmlconfig.PackageInfo;
import tal.drivechain.module.xmlconfig.ModuleInfo;

/**
 *
 * @author Main
 */
public class PluginLoader {
    private static HashMap<String,ModulePackageManager> packages = new HashMap<String,ModulePackageManager>();
    private static ModulePackageManager defaultPackage = null;
    
    private static int freeid = 0;

    public static ModulePackageManager loadPackage(String path) throws IllegalArgumentException {
        try {
            File fPath = new File(path);
            URL[] urls = new URL[1];
            urls[0]= fPath.toURI().toURL();
            if (!fPath.exists())
                throw new IllegalArgumentException("Can't find package file: " + path);
            URLClassLoader loader = new URLClassLoader(urls);
            Class m = loader.loadClass("manager.Manager");
            Object objM = m.newInstance();
            if (objM instanceof ModulePackageManager) {
                ModulePackageManager manager = (ModulePackageManager)objM;
                manager.setPath(fPath);
                packages.put(manager.getPackageConfiguration().getPackageInfo().getName().toLowerCase(), manager);
                return manager;
            } else {
                throw new IllegalArgumentException("Class Installer in " + path + " is not a valid modules package manager.");
            }
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("Can't find a package manager in " + path);
        } catch (IllegalAccessException e) { 
            throw new IllegalArgumentException("Can't load package manager in " + path + " (Illegal access)");
        } catch (InstantiationException e) { 
            throw new IllegalArgumentException("Can't load package manager in " + path + " (Can't instantiate)");
        } catch (Exception e) { 
            e.printStackTrace();
            throw new IllegalArgumentException("While loading package " + path + ": " + e.getMessage()); 
        }
    }

    /**
     * Check that the dependencies of every loaded package are present. These can be a drivechain version wildcard or a module
     * package version wildcard.
     *
     * @throws java.lang.IllegalArgumentException If a dependency is missing in the system.
     */
    public static void checkPackageDependencies() throws IllegalArgumentException {
        for (ModulePackageManager m : packages.values()) {
            PackageInfo p = m.getPackageConfiguration().getPackageInfo();
            for (String dep : p.getDependencies().keySet()) {
                if (dep.equals(Globals.VERSION.getSrcName())) {
                    Wildcard wc = p.getDependencies().get(dep);
                    if (!wc.matches(Globals.VERSION.getVersionLevel(), null))
                        throw new IllegalArgumentException("Package " + p.getName() + " failed dependency test, requires: " + dep + " " + wc.getWildcardString(null)
                                + " (found: " + Globals.VERSION.toString(true, true, true, false));
                } else {
                    Wildcard wc = p.getDependencies().get(dep);
                    ModulePackageManager q = packages.get(dep);
                    if (q==null)
                        throw new IllegalArgumentException("Missing dependency for '" + p.getName() + "': module package '" + dep);
                    else {
                        Version ver = q.getPackageConfiguration().getPackageInfo().getVersion();
                        if (!wc.matches(ver.getVersionLevel(), null))
                            throw new IllegalArgumentException("Package " + p.getName() + " failed dependency test, requires: " + dep + " " + wc.getWildcardString(null) +
                                    " (found: " + ver.toString(true, true, true, false));
                    }
                }
            }
        }
    }
    
    public static void setDefaultPackage(String name) throws IllegalArgumentException {
        defaultPackage = packages.get(name);
        if (defaultPackage==null)
            throw new IllegalArgumentException("Can't find package " + name + " in the loaded module packages list.");
    }
    
    public static ModulePackageManager getDefaultPackage() {
        return defaultPackage;
    }
    
    public static HashMap<String,ModulePackageManager> getPackageManagers() { return packages; }
    
    public static Module getModule(String name) {
        ModulePackageManager modulePackage = null;
        name = name.trim();
        
        //first look if the module exists in the default package
        ModuleInfo lookInDefault = defaultPackage.getPackageConfiguration().getModulesInfo().get(name);
        if (lookInDefault!=null) { //found in default package and used without package name
            name = defaultPackage.getPackageConfiguration().getPackageInfo().getName().toLowerCase() + "." + name;
            modulePackage = defaultPackage;
        } else { //look in every other package and search for full name
            for (ModulePackageManager manager : packages.values()) {
                for (String plugName : manager.getPackageConfiguration().getModulesInfo().keySet()) {
                    String packageName = manager.getPackageConfiguration().getPackageInfo().getName().toLowerCase();
                    if (name.equals(packageName + "." + plugName)) {
                        modulePackage = manager;
                    }
                }
            }
            // if a package containing the module name was not found:
            if (modulePackage==null)
                throw new IllegalArgumentException("Can't find module '" + name + "' in any of the packages.");
        }
        
        try {
            Class plg = getClass(name);
            if (Module.class.isAssignableFrom(plg)) {
                Module module = (Module)plg.newInstance();
                module.setPackageManager(modulePackage);
                return module;
            } else throw new IllegalArgumentException("Can't open " + name + " (not a module)");
        } catch (IllegalAccessException e) { throw new IllegalArgumentException("Can't open " + name + " (Illegal access)");
        } catch (InstantiationException e) { throw new IllegalArgumentException("Can't open " + name + " (Can't instantiate module)");
        } catch (NoClassDefFoundError e) { throw new IllegalArgumentException("Can't open " + name + " (Class definition not found)"); 
        }
    }
    
    public static ModuleInfo getModuleInfo(String plugName) {
        ModuleInfo ret = null;
        int dotIndex = plugName.indexOf('.');
        if (dotIndex==-1)
            ret = defaultPackage.getPackageConfiguration().getModulesInfo().get(plugName);
        else {
            String packName = plugName.substring(0, dotIndex);
            String firstName = plugName.substring(dotIndex+1);
            for (ModulePackageManager manager : packages.values()) {
                if (manager.getPackageConfiguration().getPackageInfo().getName().toLowerCase().equals(packName)) {
                    ret = manager.getPackageConfiguration().getModulesInfo().get(firstName);
                    break; 
                }
            }
        }
        if (ret!=null) return ret;
        else throw new IllegalArgumentException("Can't find module '" + plugName + "' in any of the packages.");
    }

    public static TreeMap<String,ModuleInfo> getAvailableModules(String packageName) throws IllegalArgumentException {
        if (packageName==null || packageName.equals("all")) {
            TreeMap<String,ModuleInfo> list = new TreeMap<String,ModuleInfo>();
            for (String name : packages.keySet()) {
                SortedMap<String,ModuleInfo> pack = getAvailableModules(name);
                list.putAll(pack);
            }
            return list;
        } else {
            ModulePackageManager manager = packages.get(packageName);
            if (manager==null) throw new IllegalArgumentException("Can't find package " + packageName);
            TreeMap<String,ModuleInfo> modules = new TreeMap<String,ModuleInfo>();
            for (ModuleInfo info : manager.getPackageConfiguration().getModulesInfo().values()) {
                String name = info.getName();
                if (!manager.equals(defaultPackage))
                    name = manager.getPackageConfiguration().getPackageInfo().getName().toLowerCase() +
                            "." + name;
                try {
                    Class modClass = getClass(name);
                    if (!Modifier.isAbstract(modClass.getModifiers()))
                        modules.put(name, info);
                } catch (IllegalArgumentException ie) {}
            }
            return modules;
        }
    }
    
    public static Logger getLogger(String name) {
        ModulePackageManager modulePackage = null;
        
        //first look if the module exists in the default package
        ModuleInfo lookInDefault = defaultPackage.getPackageConfiguration().getModulesInfo().get(name);
        if (lookInDefault!=null) { //found in default package and used without package name
            name = defaultPackage.getPackageConfiguration().getPackageInfo().getName().toLowerCase() + "." + name;
            modulePackage = defaultPackage;
        } else { //look in every other package and search for full name
            for (ModulePackageManager manager : packages.values()) {
                for (String loggerName : manager.getPackageConfiguration().getLoggersInfo().keySet()) {
                    String packageName = manager.getPackageConfiguration().getPackageInfo().getName().toLowerCase();
                    if (name.equals(packageName + "." + loggerName)) {
                        modulePackage = manager;
                    }
                }
            }
            // if a package containing the module name was not found:
            if (modulePackage==null)
                throw new IllegalArgumentException("Can't find logger '" + name + "' in any of the packages.");
        }
        
        try {
            Class clogger = getClass(name);
            Object obj = clogger.newInstance();
            if (obj instanceof Logger) {
                Logger logger = (Logger)obj;
                logger.setPackageManager(modulePackage);
                return logger;
            } else throw new IllegalArgumentException("Can't open " + name + " (not a logger)");
        } catch (IllegalAccessException e) { throw new IllegalArgumentException("Can't open " + name + " (Illegal access)");
        } catch (InstantiationException e) { throw new IllegalArgumentException("Can't open " + name + " (Can't instantiate logger)");
        } catch (NoClassDefFoundError e) { throw new IllegalArgumentException("Can't open " + name + " (Class definition not found)"); 
        }
    }
    
    public static HashMap<String, LoggerInfo> getAvailableLoggers(String packageName) {
        if (packageName==null) {
            HashMap<String,LoggerInfo> list = new HashMap<String,LoggerInfo>();
            for (String name : packages.keySet()) {
                HashMap<String,LoggerInfo> pack = getAvailableLoggers(name);
                list.putAll(pack);
            }
            return list;
        } else {
            ModulePackageManager manager = packages.get(packageName);
            if (manager==null) throw new IllegalArgumentException("Can't find package " + packageName);
            HashMap<String,LoggerInfo> loggers = new HashMap<String,LoggerInfo>();
            for (LoggerInfo info : manager.getPackageConfiguration().getLoggersInfo().values()) {
                String name = info.getName();
                if (!manager.equals(defaultPackage))
                    name = manager.getPackageConfiguration().getPackageInfo().getName().toLowerCase() +
                            "." + name;
                loggers.put(name, info);
            }
            return loggers;
        }
    }
    
    public static int getFreeId() { return freeid++; }
    
    /**
     * Adds a JAR file to the program's classpath.
     *
     * @param classpath The path to the added file.
     * @throws IOException If there was a problem loading the file into the classpath.
     */
    public static void addClassPath(String classpath) throws IOException {
        ClassPathHacker.addFile(classpath);
    }    

    public static void addLibraryPath(String librarypath) throws IOException {
        ClassPathHacker.addDir(librarypath);
    }
    public static Class getClass(String classname) throws IllegalArgumentException {
        try {
            List<URL> urls = new ArrayList<URL>();
            for (ModulePackageManager manager : packages.values()) {
                urls.add(manager.getPath().toURI().toURL());
            }
                
            URLClassLoader loader = new URLClassLoader(urls.toArray(new URL[] {}));
            if (classname.indexOf('.')==-1) classname = defaultPackage.getPackageConfiguration().getPackageInfo().getName() + "." + classname;
            return loader.loadClass(classname);
        } catch (MalformedURLException ex) {
            throw new IllegalArgumentException(ex.getMessage());
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("Can't find " + classname);
        } 
    }
}
